diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-05 16:46:43 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-05 16:46:43 +0900 |
| commit | a2c78d3a00c569a37ab93f65b58a11ba3519b596 (patch) | |
| tree | 1909ff3d52bb6f17a5b376d332255291cc71ecf5 /app/[lng]/evcp | |
| parent | 208ed7ff11d0f822d3d243c5833d31973904349e (diff) | |
(김준회) 실사의뢰/실사재의뢰 누락된 userId 추가해서 pendingActions에 추가하도록 변경
Diffstat (limited to 'app/[lng]/evcp')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx | 377 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx | 56 |
2 files changed, 433 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx new file mode 100644 index 00000000..80cf4379 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx @@ -0,0 +1,377 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { ApprovalLogDetail } from "@/lib/approval-log/service"; +import { formatDate } from "@/lib/utils"; +import { Clock, Mail, User, FileText, Shield, AlertCircle, CheckCircle, XCircle, Zap } from "lucide-react"; + +interface ApprovalLogDetailViewProps { + detail: ApprovalLogDetail; +} + +export function ApprovalLogDetailView({ detail }: ApprovalLogDetailViewProps) { + const { approvalLog, pendingAction } = detail; + + // 상태 텍스트 변환 + const getStatusText = (status: string) => { + const statusMap: Record<string, string> = { + '-3': '암호화실패', + '-2': '암호화중', + '-1': '예약상신', + '0': '보류', + '1': '진행중', + '2': '완결', + '3': '반려', + '4': '상신취소', + '5': '전결', + '6': '후완결' + }; + return statusMap[status] || '알 수 없음'; + }; + + const getStatusVariant = (status: string) => { + switch (status) { + case '2': return 'default'; // 완결 + case '3': return 'destructive'; // 반려 + case '4': return 'destructive'; // 상신취소 + case '5': return 'default'; // 전결 + case '6': return 'default'; // 후완결 + case '1': return 'secondary'; // 진행중 + default: return 'outline'; // 기타 + } + }; + + const getSecurityText = (type: string) => { + switch (type) { + case 'CONFIDENTIAL_STRICT': return '극비'; + case 'CONFIDENTIAL': return '기밀'; + case 'PERSONAL': return '개인'; + default: return type || '개인'; + } + }; + + const getSecurityVariant = (type: string) => { + switch (type) { + case 'CONFIDENTIAL_STRICT': return 'destructive'; + case 'CONFIDENTIAL': return 'secondary'; + default: return 'outline'; + } + }; + + // Pending Action 상태 텍스트 및 뱃지 + const getPendingActionStatusText = (status: string) => { + const statusMap: Record<string, string> = { + 'pending': '결재 대기 중', + 'approved': '결재 승인됨 (실행 대기)', + 'executed': '실행 완료', + 'failed': '실행 실패', + 'rejected': '결재 반려됨', + 'cancelled': '결재 취소됨', + }; + return statusMap[status] || status; + }; + + const getPendingActionStatusVariant = (status: string) => { + switch (status) { + case 'executed': return 'default'; + case 'failed': return 'destructive'; + case 'rejected': return 'destructive'; + case 'cancelled': return 'destructive'; + case 'approved': return 'secondary'; + case 'pending': return 'outline'; + default: return 'outline'; + } + }; + + // 상신일시 포맷 + const formatSbmDt = (sbmDt: string | null) => { + if (!sbmDt) return '-'; + return sbmDt.replace( + /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, + '$1-$2-$3 $4:$5:$6' + ); + }; + + return ( + <div className="space-y-6"> + {/* 결재 기본 정보 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + 결재 기본 정보 + </CardTitle> + <CardDescription>결재 문서의 기본 정보입니다.</CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <FileText className="h-4 w-4" /> + 결재 ID + </div> + <div className="font-mono text-sm bg-muted p-2 rounded"> + {approvalLog.apInfId} + </div> + </div> + + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <AlertCircle className="h-4 w-4" /> + 상태 + </div> + <div> + <Badge variant={getStatusVariant(approvalLog.status)}> + {getStatusText(approvalLog.status)} + </Badge> + </div> + </div> + + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <User className="h-4 w-4" /> + 사용자 ID + </div> + <div className="text-sm">{approvalLog.userId || '-'}</div> + </div> + + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Mail className="h-4 w-4" /> + 이메일 + </div> + <div className="text-sm">{approvalLog.emailAddress}</div> + </div> + + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Clock className="h-4 w-4" /> + 상신일시 + </div> + <div className="text-sm">{formatSbmDt(approvalLog.sbmDt)}</div> + </div> + + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Shield className="h-4 w-4" /> + 보안등급 + </div> + <div> + <Badge variant={getSecurityVariant(approvalLog.docSecuType)}> + {getSecurityText(approvalLog.docSecuType)} + </Badge> + </div> + </div> + + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Zap className="h-4 w-4" /> + 긴급여부 + </div> + <div> + {approvalLog.urgYn === 'Y' ? ( + <Badge variant="destructive">긴급</Badge> + ) : ( + <span className="text-sm">일반</span> + )} + </div> + </div> + + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <FileText className="h-4 w-4" /> + 본문종류 + </div> + <div className="text-sm">{approvalLog.contentsType}</div> + </div> + </div> + + <Separator /> + + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">제목</div> + <div className="text-base font-medium">{approvalLog.subject}</div> + </div> + + {approvalLog.opinion && ( + <> + <Separator /> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">상신의견</div> + <div className="text-sm">{approvalLog.opinion}</div> + </div> + </> + )} + </CardContent> + </Card> + + {/* 결재선 정보 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <User className="h-5 w-5" /> + 결재선 정보 + </CardTitle> + <CardDescription>결재 승인 라인 정보입니다.</CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-2"> + <pre className="text-xs bg-muted p-4 rounded overflow-auto max-h-[400px]"> + {JSON.stringify(approvalLog.aplns, null, 2)} + </pre> + </div> + </CardContent> + </Card> + + {/* 결재 본문 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + 결재 본문 + </CardTitle> + <CardDescription>결재 문서의 상세 내용입니다.</CardDescription> + </CardHeader> + <CardContent> + {approvalLog.contentsType === 'HTML' ? ( + <div + className="prose prose-sm max-w-none dark:prose-invert" + dangerouslySetInnerHTML={{ __html: approvalLog.content }} + /> + ) : ( + <pre className="text-sm whitespace-pre-wrap bg-muted p-4 rounded"> + {approvalLog.content} + </pre> + )} + </CardContent> + </Card> + + {/* Pending Action 정보 */} + {pendingAction && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <CheckCircle className="h-5 w-5" /> + 액션 정보 + </CardTitle> + <CardDescription> + 결재와 연결된 Pending Action 정보입니다. + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">액션 ID</div> + <div className="font-mono text-sm">{pendingAction.id}</div> + </div> + + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">액션 타입</div> + <div className="text-sm font-medium">{pendingAction.actionType}</div> + </div> + + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">상태</div> + <div> + <Badge variant={getPendingActionStatusVariant(pendingAction.status)}> + {getPendingActionStatusText(pendingAction.status)} + </Badge> + </div> + </div> + + {pendingAction.executedAt && ( + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">실행 시간</div> + <div className="text-sm">{formatDate(pendingAction.executedAt)}</div> + </div> + )} + + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">생성일</div> + <div className="text-sm">{formatDate(pendingAction.createdAt)}</div> + </div> + + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">수정일</div> + <div className="text-sm">{formatDate(pendingAction.updatedAt)}</div> + </div> + </div> + + <Separator /> + + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">액션 페이로드</div> + <pre className="text-xs bg-muted p-4 rounded overflow-auto max-h-[300px]"> + {JSON.stringify(pendingAction.actionPayload, null, 2)} + </pre> + </div> + + {pendingAction.executionResult && ( + <> + <Separator /> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">실행 결과</div> + <pre className="text-xs bg-muted p-4 rounded overflow-auto max-h-[300px]"> + {JSON.stringify(pendingAction.executionResult, null, 2)} + </pre> + </div> + </> + )} + + {pendingAction.errorMessage && ( + <> + <Separator /> + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm font-medium text-destructive"> + <XCircle className="h-4 w-4" /> + 에러 메시지 + </div> + <div className="text-sm text-destructive bg-destructive/10 p-3 rounded"> + {pendingAction.errorMessage} + </div> + </div> + </> + )} + </CardContent> + </Card> + )} + + {/* 메타데이터 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Clock className="h-5 w-5" /> + 메타데이터 + </CardTitle> + <CardDescription>생성 및 수정 정보입니다.</CardDescription> + </CardHeader> + <CardContent> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">생성일</div> + <div className="text-sm">{formatDate(approvalLog.createdAt)}</div> + </div> + + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">수정일</div> + <div className="text-sm">{formatDate(approvalLog.updatedAt)}</div> + </div> + + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">타임존</div> + <div className="text-sm">{approvalLog.timeZone}</div> + </div> + + <div className="space-y-2"> + <div className="text-sm text-muted-foreground">상신언어</div> + <div className="text-sm">{approvalLog.sbmLang}</div> + </div> + </div> + </CardContent> + </Card> + </div> + ); +} + diff --git a/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx new file mode 100644 index 00000000..3567d87a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx @@ -0,0 +1,56 @@ +import { notFound } from "next/navigation"; +import { Shell } from "@/components/shell"; +import { getApprovalLogDetail } from "@/lib/approval-log/service"; +import { ApprovalLogDetailView } from "./approval-log-detail-view"; +import { InformationButton } from "@/components/information/information-button"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft } from "lucide-react"; +import Link from "next/link"; + +interface ApprovalLogDetailPageProps { + params: { + lng: string; + apInfId: string; + }; +} + +export default async function ApprovalLogDetailPage({ + params, +}: ApprovalLogDetailPageProps) { + const { lng, apInfId } = params; + + // 상세 정보 조회 + const detail = await getApprovalLogDetail(apInfId); + + if (!detail) { + notFound(); + } + + return ( + <Shell className="gap-4"> + <div className="flex items-center justify-between"> + <div className="flex items-center gap-4"> + <Link href={`/${lng}/evcp/approval/log`}> + <Button variant="ghost" size="icon"> + <ArrowLeft className="h-4 w-4" /> + </Button> + </Link> + <div> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight"> + 결재 로그 상세 + </h2> + <InformationButton pagePath="evcp/approval/log" /> + </div> + <p className="text-sm text-muted-foreground mt-1"> + {detail.approvalLog.subject} + </p> + </div> + </div> + </div> + + <ApprovalLogDetailView detail={detail} /> + </Shell> + ); +} + |
